<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
        <title>AJ Security/API 接口数据加密</title>
        <meta name="description" content="A Practical Java Web Security Library. 对 HTTP API 接口中的入参、出参进行数据加密、解密，其目的是保护敏感数据、防篡改、防抓包。"/>
        <meta name="keywords" content="security, xss, csrf, captcha, api"/>
        <meta name="viewport" content="width=device-width, initial-scale=1"/>
         <link rel="stylesheet" href="https://framework.ajaxjs.com/static/font/font.css" />
        <link rel="stylesheet" href="/asset/main.css"/>
        <link rel="icon" type="image/x-icon" href="https://framework.ajaxjs.com/aj-logo/logo.ico"/>
        <script src="https://framework.ajaxjs.com/static/aj-docs/common.js"></script>
        <script>
            // 获取用户的默认语言
            var userLang = navigator.language || navigator.userLanguage;

            // 检查是否为中文环境（包括简体和繁体）
            if (userLang.startsWith('zh') && location.pathname.indexOf('cn') == -1) {
                 confirm('欢迎！您可以改为访问中文内容。是否继续？') && location.assign('/cn');  // 如果是中文，则弹出提示
            }

            var _hmt = _hmt || [];
            (function() {
              var hm = document.createElement("script");
              hm.src = "https://hm.baidu.com/hm.js?950ba5ba1f1fe4906c3b4cf836080f03";
              var s = document.getElementsByTagName("script")[0];
              s.parentNode.insertBefore(hm, s);
            })();
        </script>
    </head>
    <body>
        <nav>
            <div>
                <div class="links">
                    <a href="/">🏠 Home</a>
                    | ⚙️ Source:
                    <a target="_blank" href="https://github.com/lightweight-component/aj-security">Github</a>/<a target="_blank" href="https://gitcode.com/lightweight-component/aj-security">Gitcode</a>
                    |
                    <a href="/cn">Chinese Version</a>
                </div>
                <h1><img src="https://framework.ajaxjs.com/aj-logo/logo.png" style="vertical-align: middle;height: 45px;margin-bottom: 6px;" /> AJ Security</h1>
                <h3>User Manual</h3>
            </div>
        </nav>
        <div>
            <menu>
                <ul>
                    <li class="selected">
                        <a href="/">Home</a>
                    </li>
                    <li>
                        <a href="/install">Installation & Configuration</a>
                    </li>
                </ul>
                <h3>HTTP Web Security</h3>
                <ul>
                    <li>
                        <a href="/http/http-referer">HTTP Referer Validation</a>
                    </li>
                    <li>
                        <a href="/http/timestamp">Timestamp Encrypted Token Validation</a>
                    </li>
         <li>
                       <a href="/http/paramssign">Parameter Signature</a>
                    </li>
                    <li>
                        <a href="/http/ip-list">IP Whitelist/Blacklist</a>
                    </li>
                    <li>
                        <a href="/http/nonrepeatsubmit">Prevent Duplicate Submission</a>
                    </li>
                </ul>
                <h3>General Web Validation</h3>
                <ul>
                    <li>
                        <a href="/classic/xss">Prevent XSS Attacks</a>
                    </li>
                    <li>
                        <a href="/classic/crlf">Prevent CRLF Attacks</a>
                    </li>
                </ul>

                <h3>Captcha Mechanism</h3>
                <ul>
                    <li><a href="/captcha/img-captcha">Image Captcha</a></li>
                    <li><a href="/captcha/google">Google-based Captcha</a></li>
                    <li><a href="/captcha/cf">CloudFlare-based Captcha</a></li>
                </ul>
                <h3>HTTP Standard Authentication</h3>
                <ul>
                    <li><a href="/auth/http-basic-auth">HTTP Basic Auth</a></li>
                    <li><a href="/auth/http-digest-auth">HTTP Digest Auth</a></li>
                </ul>
                <h3>API Features</h3>
                <ul>
                    <li><a href="/api/limit">Rate Limiting</a></li>
                </ul>
                <h3>Other Practical Features</h3>
                <ul>
                    <li><a href="/misc/desensitize">Field Desensitization</a></li>
                    <li><a href="/misc/encryption-api">API Encryption</a></li>
                    <li><a href="/misc/trace-id">Trace Tracking</a></li>
                </ul>
            </menu>
            <article>
                <h1>API Interface Encryption/Decryption</h1>
<p>To ensure data security, API interface data must be encrypted to prevent plaintext exposure. Encryption and decryption should be applied to API interfaces as follows:</p>
<ul>
<li><strong>Request Parameters</strong>: Decrypt encrypted parameters received from the client. The HTTP Raw Body (encrypted ciphertext) is converted to JSON for processing, as it is the most common submission method. Other input methods like QueryString, standard Form, or HTTP Header parameters are not supported.</li>
<li><strong>Response Parameters</strong>: Encrypt the response data before returning it. The interface consistently returns encrypted JSON results.</li>
</ul>
<p>Some approaches output the plaintext of the encryption result, as shown below:</p>
<p><img src="/asset/imgs/api-encode.png" alt=""></p>
<p>However, this approach is considered an anti-pattern. It is better to retain the original JSON structure, as shown in the submitted JSON below:</p>
<pre><code class="language-json">{
    &quot;errCode&quot;: &quot;0&quot;,
    &quot;data&quot;: &quot;BQduoGH4PI+6jxgu+6S2FWu5c/vHd+041ITnCH9JulUKpPX8BvRTvBNYfP7……&quot;
}
</code></pre>
<p>This aligns with the existing unified response format, where only the <code>data</code> field is encrypted, while other fields like <code>code</code> and <code>msg</code> are displayed normally.</p>
<p>System Requirements: Only supports Spring + Jackson.</p>
<h2>Encryption Algorithm</h2>
<p>The encryption algorithm must be agreed upon by the caller (e.g., browsers) and the API interface. Typically, RSA encryption is used. Although RSA is slower than AES, it is advantageous due to its asymmetric encryption nature. AES, being a symmetric encryption mechanism, is unsuitable for this scenario because browsers cannot store any keys—except asymmetric public keys.</p>
<p>If the API interface is designed for third-party calls instead of browsers and can ensure key security, AES can be used. Similarly, other hashing algorithms (e.g., MD5, SHA1, SHA256) can be applied, provided the algorithm and salt value are agreed upon.</p>
<p>Currently, the component only supports RSA (1024-bit key). More algorithms are planned, including:</p>
<ul>
<li>RSA (512/2048…)</li>
<li>AES</li>
<li>MD5/SHA1/SHA256… with Salt</li>
</ul>
<h1>Usage</h1>
<h2>Initialization</h2>
<p>Add the following configuration in YAML:</p>
<pre><code class="language-yaml">api:
  EncryptedBody:
    enable: true
    publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmkKluNutOWGmAK2U……
    privateKey: MIICdgIBADANBgkqhkiG9w0BAQ……
</code></pre>
<p>The main configuration includes RSA public/private keys. Then, add the following to the Spring configuration class <code>WebMvcConfigurer</code>:</p>
<pre><code class="language-java">@Value(&quot;${api.EncryptedBody.publicKey}&quot;)
private String apiPublicKey;

@Value(&quot;${api.EncryptedBody.privateKey}&quot;)
private String apiPrivateKey;

@Value(&quot;${api.EncryptedBody.enable}&quot;)
private boolean apiEncryptedBodyEnable;

@Override
public void configureMessageConverters(List&lt;HttpMessageConverter&lt;?&gt;&gt; converters) {
    EncryptedBodyConverter converter = new EncryptedBodyConverter(apiPublicKey, apiPrivateKey);
    converter.setEnabled(apiEncryptedBodyEnable);

    converters.add(0, converter);
}
</code></pre>
<h2>Configure Encrypted Data</h2>
<p>The usage is straightforward: add a custom Java annotation <code>@EncryptedData</code> to your Java Bean.</p>
<h3>Decrypting Request Data</h3>
<p>Observe the following Spring MVC interface declaration, which uses the standard JSON submission method with the <code>@RequestBody</code> annotation:</p>
<pre><code class="language-java">@PostMapping(&quot;/submit&quot;)
boolean jsonSubmit(@RequestBody User user);
</code></pre>
<p>The key part is the <code>User</code> DTO. To indicate encrypted data, declare the custom annotation <code>@EncryptedData</code> on the Bean:</p>
<pre><code class="language-java">package com.ajaxjs.api.encryptedbody;

@EncryptedData
public class User {
    private String name;
    private int age;

    // Getters and Setters
}
</code></pre>
<p>Client submissions are no longer in the form of <code>User</code> JSON but rather <code>DecodeDTO</code> (which is ultimately converted to <code>User</code> upon successful decryption):</p>
<pre><code class="language-java">package com.ajaxjs.api.encryptedbody;

import lombok.Data;

@Data
public class DecodeDTO {
    /**
     * Encrypted data
     */
    private String data;
}
</code></pre>
<p>The typical submission format is as follows:</p>
<pre><code class="language-json">{
    &quot;data&quot;: &quot;BQduoGH4PI+6jxgu+6S2FWu5c/vHd+041ITnCH9JulUKpPX8BvRTvBNYfP7……&quot;
}
</code></pre>
<p>The encrypted ciphertext is generated by the client after encryption, or it can be returned using the method described in the following section.</p>
<h3>Encrypting Response Data</h3>
<p>The following controller method returns a <code>User</code> object without any modifications:</p>
<pre><code class="language-java">@GetMapping(&quot;/user&quot;)
User User();

……

@Override
public User User() {
    User user = new User();
    user.setAge(1);
    user.setName(&quot;tom&quot;);
    
    return user;
}
</code></pre>
<p>Add the annotation <code>@EncryptedData</code> to encrypt the response object. Currently, field-level encryption is not supported; only entire object encryption is available.</p>
<p>The response example is as follows:</p>
<pre><code class="language-json">{
    &quot;status&quot;: 1,
    &quot;errorCode&quot;: null,
    &quot;message&quot;: &quot;Operation successful&quot;,
    &quot;data&quot;: &quot;ReSSPC34JE+O/SmLCxE5zVJb6D2tzp1f5pfQyKdjvOWkQQ+qDjcjw/2m/KPA+2+uc9kseqFryXNPIZCEfsaOCJAqzMtrXyZ0JPB1skeJxKOngS5USijsY0UZqN9hLS3O/7CBLlSGkEuyXZV//WcWDG9BpQ4TAKrlRfwM4bnCo+E=&quot;
}
</code></pre>
<h2>Add Dependencies</h2>
<p>Don't forget to add dependencies! Since no standalone jar package is provided, simply copy the source code—it's only three classes: <a href="https://gitcode.com/zhangxin09/aj-framework/tree/master/aj-framework/src/main/java/com/ajaxjs/api/encryptedbody">Source Code</a>.</p>
<p>The <code>ResponseResultWrapper</code> class is for unified response results. You can adapt this to your project's requirements. The RSA dependency uses the following utility package:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;com.ajaxjs&lt;/groupId&gt;
    &lt;artifactId&gt;ajaxjs-util&lt;/artifactId&gt;
    &lt;version&gt;1.1.8&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>It's a lightweight package, only 60 KB—safe to use!</p>
<h1>Implementation Details</h1>
<p>Here are the implementation principles and design considerations.</p>
<p>This usage essentially involves receiving Object A (encrypted, <code>DecodeDTO</code>) and converting it to Object B (decrypted, used by the controller). The simplest approach is as follows:</p>
<pre><code class="language-java">@PostMapping(&quot;/submit&quot;)
boolean jsonSubmit(@RequestBody DecodeDTO dto) {
    User user = conversionFunction(dto.getData());
}
</code></pre>
<p>However, this method results in numerous <code>DecodeDTO</code> instances scattered across the codebase, complicating API documentation and reducing clarity. Instead, a non-invasive approach is preferred. Non-invasiveness means not modifying existing code, only adding &quot;decorations.&quot; Common techniques include AOP. Similar libraries (<a href="https://github.com/ishuibo/rsa-encrypt-body-spring-boot">rsa-encrypt-body-spring-boot</a>, <a href="https://github.com/Licoy/encrypt-body-spring-boot-starter">encrypt-body-spring-boot-starter</a>) also adopt AOP.</p>
<p>Despite its popularity, the author prefers to avoid AOP whenever possible. Alternatives like filters and interceptors were considered but ultimately deemed less suitable. Instead, the focus shifted to JSON serialization/deserialization layers, where encryption/decryption could be performed before serialization/deserialization.</p>
<p>The result is <code>EncryptedBodyConverter</code>, an extension of <code>MappingJackson2HttpMessageConverter</code>. The <code>read</code> method performs decryption before deserialization, and the <code>writeInternal</code> method handles encryption.</p>
<p>The core implementation is just one class, under 100 lines of code:</p>
<pre><code class="language-java">import com.ajaxjs.springboot.ResponseResultWrapper;
import com.ajaxjs.util.EncodeTools;
import com.ajaxjs.util.cryptography.RsaCrypto;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

import java.io.IOException;
import java.lang.reflect.Type;

public class EncryptedBodyConverter extends MappingJackson2HttpMessageConverter {
    public EncryptedBodyConverter(String publicKey, String privateKey) {
        super();
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }

    private final String publicKey;

    private final String privateKey;

    /**
     * Decrypts a string using the private key.
     *
     * @param encryptBody Encrypted string, Base64 encoded.
     * @param privateKey  Private key string for decryption.
     * @return Decrypted string.
     */
    static String decrypt(String encryptBody, String privateKey) {
        byte[] data = EncodeTools.base64Decode(encryptBody);

        return new String(RsaCrypto.decryptByPrivateKey(data, privateKey));
    }

    /**
     * Encrypts a string using the public key.
     *
     * @param body      Original string to be encrypted.
     * @param publicKey Public key string for encryption.
     * @return Encrypted string, Base64 encoded.
     */
    static String encrypt(String body, String publicKey) {
        byte[] encWord = RsaCrypto.encryptByPublicKey(body.getBytes(), publicKey);
        return EncodeTools.base64EncodeToString(encWord);
    }

    /**
     * Overrides the `read` method to support encrypted data reading.
     *
     * @param type         Data type for deserialization.
     * @param contextClass Context class, unused here.
     * @param inputMessage HTTP input message containing encrypted data.
     * @return Deserialized object instance.
     * @throws IOException                     If an I/O error occurs during reading.
     * @throws HttpMessageNotReadableException If the message cannot be parsed into an object.
     */
    @Override
    public Object read(Type type, Class&lt;?&gt; contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        Class&lt;?&gt; clz = (Class&lt;?&gt;) type;

        if (clz.getAnnotation(EncryptedData.class) != null) {
            ObjectMapper objectMapper = getObjectMapper();
            DecodeDTO decodeDTO = objectMapper.readValue(inputMessage.getBody(), DecodeDTO.class);
            String encryptBody = decodeDTO.getData();

            String decodeJson = decrypt(encryptBody, privateKey);

            return objectMapper.readValue(decodeJson, clz);
        }

        return super.read(type, contextClass, inputMessage);
    }

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        Class&lt;?&gt; clz = (Class&lt;?&gt;) type;

        if (object instanceof ResponseResultWrapper &amp;&amp; clz.getAnnotation(EncryptedData.class) != null) {
            ResponseResultWrapper response = (ResponseResultWrapper) object;
            Object data = response.getData();
            String json = getObjectMapper().writeValueAsString(data);
            String encryptBody = encrypt(json, publicKey);

            response.setData(encryptBody);
        }

        super.writeInternal(object, type, outputMessage);
    }
}
</code></pre>
<h3>TODO</h3>
<ul>
<li>Expand encryption and decryption algorithms.</li>
<li>Add an encryption option to specify whether to use public or private keys (currently using public keys).</li>
<li>Support field-level encryption.</li>
</ul>

            </article>
        </div>
        <footer>
            AJ Security, a part of
            <a href="https://framework.ajaxjs.com" target="_blank">AJ-Framework</a>
            open source. Mail:frank@ajaxjs.com, visit
            <a href="https://blog.csdn.net/zhangxin09" target="_blank">my blog(In Chinese)</a>. <br/> <br/> Copyright © 2025 Frank Cheung. All rights reserved.
        </footer>
    </body>
</html>